﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Samples
{
    #region Enums
    /// <summary>
    /// Defines style of camera
    /// </summary>
    public enum CameraStyle
    {
        /// <summary>
        /// always toward a target
        /// </summary>
        TargetBased,
        /// <summary>
        /// Handle rotation and position manually
        /// </summary>
        PositionBased,        
    }
    #endregion
    /// <summary>
    /// Summary description for Camera.
    /// </summary>
    public class Camera
    {
        #region Events
        /// <summary>
        /// Occures when farplane changed
        /// </summary>
        public event EventHandler FarChanged;
        protected void RaiseFarChanged()
        {
            if (FarChanged != null)
                FarChanged(this, EventArgs.Empty);
        }


        /// <summary>
        /// Occures when camera changed
        /// </summary>
        public event EventHandler CameraChanged;
        protected void RaiseCameraChanged()
        {
            if (CameraChanged != null)
                CameraChanged(this, EventArgs.Empty);
        }
        #endregion

        #region Variables
        private bool viewInverseCreated = false;
        private bool viewInverseTransposeCreated = false;

        protected Matrix viewProjection;
        protected Matrix view;
        protected Matrix viewInverse;
        protected Matrix viewInverseTranspose;
        protected Matrix projection;
        protected Vector3 position;
        protected Vector3 look;
        protected Vector3 up;
        protected Vector3 right;

        private CameraStyle currentStyle = CameraStyle.PositionBased;        
        private bool changed;
        private float minZoomDist;
        private Vector3 planeNormal;
        private Vector3 planeVec;
        private float rotationSpeed;
        private Viewport viewport;
        private Vector3 target;
        private float zoomDist;
        private float zoomSpeed;

        protected float fov = MathHelper.PiOver4;
        protected float aspectratio = 1.33f;
        protected float farPlane = 128.0f;
        protected float nearPlane = 0.01f;

        protected float yaw, pitch, roll;
        private BoundingFrustum frustum;
        #endregion

        #region Properties

        /// <summary>
        /// Gets or set viewport
        /// </summary>
        public Viewport Viewport
        {
            get { return viewport; }
            set
            {
                viewport = value;
                if (aspectratio != value.AspectRatio)
                {
                    aspectratio = value.AspectRatio;
                    projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectratio, nearPlane, farPlane);
                }
            }
        }

        /// <summary>
        /// retrieve frusum of camera
        /// </summary>
        public BoundingFrustum Frustum { get { return frustum; } }

        /// <summary>
        /// get view matrix
        /// </summary>
        public Matrix View { get { return view; } }
        /// <summary>
        /// get projection matrix
        /// </summary>
        public Matrix Projection { get { return projection; } }


        /// <summary>
        /// get or set field of view
        /// </summary>
        public virtual float FieldOfView
        {
            get { return fov; }
            set
            {
                fov = value;
                projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectratio, nearPlane, farPlane);
            }
        }
        /// <summary>
        /// get or set near clip plane
        /// </summary>
        public virtual float NearClipPlane
        {
            get { return nearPlane; }
            set
            {
                nearPlane = value;
                projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectratio, nearPlane, farPlane);
            }
        }
        /// <summary>
        /// Retrieve far clip plane
        /// </summary>
        public virtual float FarClipPlane
        {
            get { return farPlane; }
            set
            {
                if (farPlane != value)
                {
                    farPlane = value;
                    projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectratio, nearPlane, farPlane);
                    RaiseFarChanged();
                }
            }
        }
        /// <summary>
        /// get view projection matrix
        /// </summary>
        public Matrix ViewProjection { get { return viewProjection; } }

        /// <summary>
        /// get view inverse matrix
        /// </summary>
        public Matrix ViewInverse
        {
            get
            {
                if (viewInverseCreated == false)
                {
                    Matrix.Invert(ref view, out viewInverse);
                    viewInverseCreated = true;
                }
                return viewInverse;
            }
        }

        /// <summary>
        /// get view inverse transpose matrix
        /// </summary>
        public Matrix ViewInverseTranspose
        {
            get
            {
                if (viewInverseTransposeCreated == false)
                {
                    if (viewInverseCreated == false)
                    {
                        Matrix.Invert(ref view, out viewInverse);
                        viewInverseCreated = true;
                    }
                    Matrix.Transpose(ref viewInverse, out viewInverseTranspose);
                    viewInverseTransposeCreated = true;
                }
                return viewInverseTranspose;
            }
        }



        /// <summary>
        /// get or set amount of yaw angle in radian
        /// </summary>
        public virtual float Yaw
        {
            get
            {
                return this.yaw;
            }
            set
            {
                if (value != this.yaw)
                {
                    yaw = value;
                    if (yaw > MathHelper.TwoPi)
                        yaw -= MathHelper.TwoPi;
                    else if (yaw < 0)
                        yaw += MathHelper.TwoPi;
                    this.changed = true;
                    if (this.currentStyle == CameraStyle.PositionBased)
                    {
                        CreateDirections();
                    }
                }
            }
        }

        /// <summary>
        /// get or set amount of roll angle in radian
        /// </summary>
        public virtual float Roll
        {
            get
            {
                return this.roll;
            }
            set
            {
                if (roll != value)
                {
                    roll = value;
                    if (roll > MathHelper.TwoPi)
                        roll -= MathHelper.TwoPi;
                    else if (roll < 0)
                        roll += MathHelper.TwoPi;
                    this.changed = true;
                    if (this.currentStyle == CameraStyle.PositionBased)
                    {
                        CreateDirections();
                    }
                }
            }
        }

        /// <summary>
        /// get or set amount of pitch angle in radian
        /// </summary>
        public virtual float Pitch
        {
            get
            {
                return this.pitch;
            }
            set
            {
                if (value != this.pitch)
                {
                    pitch = value;
                    if (pitch > MathHelper.PiOver2)
                        pitch = MathHelper.PiOver2;
                    else if (pitch < -MathHelper.PiOver2)
                        pitch = -MathHelper.PiOver2;
                    this.changed = true;
                    if (this.currentStyle == CameraStyle.PositionBased)
                    {
                        CreateDirections();
                    }
                }
            }
        }

        protected void CreateDirections()
        {
            Matrix matrix = Matrix.CreateFromYawPitchRoll(this.yaw, this.pitch, this.roll);
            this.Direction = matrix.Forward;
            this.Up = matrix.Up;
            this.right = matrix.Right;
        }

        /// <summary>
        /// Gets or set direction (look) vector
        /// </summary>
        public Vector3 Direction
        {
            get
            {
                return this.look;
            }
            set
            {
                this.look = value;
                this.changed = true;
                if (this.currentStyle == CameraStyle.PositionBased)
                {
                    this.target = Vector3.Add(this.position, this.look);
                }
            }
        }

        /// <summary>
        /// Retrieve right vector
        /// </summary>
        public Vector3 Right { get { return right; } }        

        /// <summary>
        /// get or set position of camera
        /// </summary>
        public Vector3 Position
        {
            get
            {
                return this.position;
            }
            set
            {
                this.position = value;
                this.changed = true;
                if (this.currentStyle == CameraStyle.PositionBased)
                {
                    this.target = Vector3.Add(this.position, this.look);
                }
            }
        }

        /// <summary>
        /// Gets or set up vector
        /// </summary>
        public Vector3 Up
        {
            get
            {
                return this.up;
            }
            set
            {
                this.up = value;
                this.changed = true;
            }
        }

        /// <summary>
        /// Gets or set Mininum Zoom Distance
        /// </summary>
        public float MinZoomDistance
        {
            get
            {
                return this.minZoomDist;
            }
            set
            {
                this.minZoomDist = value;
            }
        }

        /// <summary>
        /// Gets or set speed of rotation
        /// </summary>
        public float RotationFactor
        {
            get
            {
                return this.rotationSpeed;
            }
            set
            {
                this.rotationSpeed = value;
            }
        }

        /// <summary>
        /// Gets or set style of camera
        /// </summary>
        public CameraStyle Style
        {
            get
            {
                return this.currentStyle;
            }
            set
            {
                this.currentStyle = value;
                this.changed = true;
            }
        }

        public void SetTargetAndPosition(Vector3 target, Vector3 position)
        {
            this.target = target;
            this.position = position;
            this.changed = true;
            if (this.currentStyle == CameraStyle.PositionBased)
            {
                this.look = Vector3.Subtract(this.target, this.position);
                this.look.Normalize();
            }
        }

        /// <summary>
        /// Gets or set target postion in targetbase style
        /// </summary>
        public Vector3 TargetPosition
        {
            get
            {
                return this.target;
            }
            set
            {
                if (!this.target.Equals(value))
                {
                    this.target = value;
                    this.changed = true;
                    if (this.currentStyle == CameraStyle.PositionBased)
                    {
                        this.look = Vector3.Subtract(this.target, this.position);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or set Maximum Zoom Distance
        /// </summary>
        public float ZoomDistance
        {
            get
            {
                return this.zoomDist;
            }
            set
            {
                if (this.zoomDist != value)
                {
                    this.zoomDist = value;
                    this.changed = true;
                }
            }
        }

        /// <summary>
        /// Gets or set speed of zooming
        /// </summary>
        public float ZoomFactor
        {
            get
            {
                return this.zoomSpeed;
            }
            set
            {
                this.zoomSpeed = value;
            }
        }

        #endregion

        #region Contructors

        /// <summary>
        /// camera
        /// </summary>
        /// <param name="position">position of camera</param>
        /// <param name="yaw">angle of yaw in radian</param>
        /// <param name="pitch">angle of pitch in radian</param>
        /// <param name="roll">angle of roll in radian</param>
        /// <param name="up">camera up vector</param>
        public Camera(Vector3 position, float yaw, float pitch, float roll, Vector3 up, float farPlane)
        {
            this.farPlane = farPlane;
            frustum = new BoundingFrustum(viewProjection);
            this.position = position;
            this.yaw = yaw;
            this.pitch = pitch;
            this.roll = roll;
            this.up = up;
            this.currentStyle = CameraStyle.PositionBased;
            this.minZoomDist = 0.01f;
            this.planeVec = Vector3.Zero;
            this.planeNormal = new Vector3(0f, 1f, 0f);
            this.rotationSpeed = 0.01f;
            this.zoomSpeed = 0.01f;
            this.zoomDist = 1f;
            this.changed = true;
            projection = Matrix.CreatePerspectiveFieldOfView(fov, aspectratio, nearPlane, farPlane);
            CreateDirections();            
        }
        /// <summary>
        /// camera default look vector is (0,0,1)
        /// </summary>
        /// <param name="position">position of camera</param>
        /// <param name="up">camera up vector</param>
        public Camera(Vector3 position, Vector3 up, float farPlane)
            : this(position, 0, 0, 0, up, farPlane)
        {
        }
        #endregion

        #region Virtual methods
        /// <summary>
        /// Move camera
        /// </summary>
        /// <param name="deltaX">move along left axis</param>
        /// <param name="deltaY">move along up axis</param>
        /// <param name="deltaZ">move along look axis</param>
        public virtual void Move(float deltaX, float deltaY, float deltaZ)
        {
            if (deltaY != 0)
                this.Position += Vector3.Multiply(up, deltaY);
            if (deltaZ != 0)
                this.Position += Vector3.Multiply(look, deltaZ);
            if (deltaX != 0)
                this.Position += Vector3.Multiply(right, deltaX);
        }
        #endregion

        #region Methods
        /// <summary>
        /// Return ray by specified point in screen
        /// </summary>
        /// <param name="x">Position X in screen</param>
        /// <param name="y">Position Y in screen</param>
        /// <returns>Ray</returns>
        public Ray GetRay(int x, int y)
        {

            float px = (((2f * x) / (float)viewport.Width) - 1.0f) / projection.M11;
            float py = -(((2f * y) / (float)viewport.Height) - 1.0f) / projection.M22;

            Vector3 direction = new Vector3(px, py, -1);
            Vector3 outDirection;

            Matrix m = Matrix.Invert(view);
            Vector3.TransformNormal(ref direction, ref m, out outDirection);
            Ray r = new Ray(position, outDirection);
            return r;
        }

        /// <summary>
        /// Gets ray by specified point in screen
        /// </summary>
        /// <param name="x">Position X in screen</param>
        /// <param name="y">Position Y in screen</param>
        /// <param name="ray">Ray to fill information</param>        
        //public void GetRay(int x, int y, ref  Dust.ODE.Geometries.Ray ray)
        //{
        //    float px = (((2f * x) / (float)viewport.Width) - 1.0f) / projection.M11;
        //    float py = -(((2f * y) / (float)viewport.Height) - 1.0f) / projection.M22;

        //    Dust.ODE.Vector3 direction = new Dust.ODE.Vector3(px, py, -1);
        //    Dust.ODE.Vector3 outDirection;

        //    Matrix m = Matrix.Invert(view);
        //    Dust.ODE.Vector3.TransformNormal(ref direction, ref m, out outDirection);
        //    ray.Direction = outDirection;
        //    ray.Position = Dust.ODE.Vector3.FromXna(position);
        //}

        /// <summary>
        /// Set yaw , pitch and roll of camera
        /// </summary>
        /// <param name="yaw">Yaw (in radian)</param>
        /// <param name="pitch">Pitch (in radian)</param>
        /// <param name="roll">Roll (in radian)</param>
        public void SetYawPitchRoll(float yaw, float pitch, float roll)
        {
            if (yaw != this.yaw)
            {
                this.yaw = yaw;
                if (this.yaw > MathHelper.TwoPi)
                    this.yaw -= MathHelper.TwoPi;
                else if (this.yaw < 0)
                    this.yaw += MathHelper.TwoPi;
                this.changed = true;
            }
            if (pitch != this.pitch)
            {
                this.pitch = pitch;
                if (this.pitch > MathHelper.PiOver2)
                    this.pitch = MathHelper.PiOver2;
                else if (this.pitch < -MathHelper.PiOver2)
                    this.pitch = -MathHelper.PiOver2;
                this.changed = true;
            }
            if (this.roll != roll)
            {
                this.roll = roll;
                if (this.roll > MathHelper.TwoPi)
                    this.roll -= MathHelper.TwoPi;
                else if (this.roll < 0)
                    this.roll += MathHelper.TwoPi;
                this.changed = true;
            }
            if (this.currentStyle == CameraStyle.PositionBased)
            {
                CreateDirections();
            }
        }

        ///// <summary>
        ///// Gets ray by specified point in screen
        ///// </summary>
        ///// <param name="x">Position X in screen</param>
        ///// <param name="y">Position Y in screen</param>
        ///// <param name="ray">Ray to fill information</param>        
        //public void GetRayViewport(int x, int y, ref  Dust.ODE.Geometries.Ray ray)
        //{
        //    // create 2 positions in screenspace using the cursor position. 0 is as
        //    // close as possible to the camera, 1 is as far away as possible.
        //    Vector3 nearSource = new Vector3(x, y, 0f);
        //    Vector3 farSource = new Vector3(x, y, 1f);

        //    // use Viewport.Unproject to tell what those two screen space positions
        //    // would be in world space. we'll need the projection matrix and view
        //    // matrix, which we have saved as member variables. We also need a world
        //    // matrix, which can just be identity.
        //    Vector3 nearPoint = viewport.Unproject(nearSource,
        //        projection, view, Matrix.Identity);

        //    Vector3 farPoint = viewport.Unproject(farSource,
        //        projection, view, Matrix.Identity);

        //    // find the direction vector that goes from the nearPoint to the farPoint
        //    // and normalize it....
        //    Vector3 direction = farPoint - nearPoint;
        //    direction.Normalize();

        //    ray.Direction = Dust.ODE.Vector3.FromXna(direction);
        //    ray.Position = Dust.ODE.Vector3.FromXna(nearPoint);
        //}
        #endregion

        #region Update
        public void ForceUpdate()
        {
            this.changed = true;
            this.Update();
        }
        /// <summary>
        /// process camera
        /// </summary>
        protected virtual void Process()
        {

            switch (this.currentStyle)
            {
                case CameraStyle.TargetBased:
                    Matrix matrix = Matrix.CreateRotationX(this.pitch) * Matrix.CreateRotationY(this.yaw);
                    look = matrix.Forward;
                    look.Normalize();
                    up = matrix.Up;
                    up.Normalize();
                    right = matrix.Right;
                    right.Normalize();
                    this.position.X = this.target.X - (this.zoomDist * this.look.X);
                    this.position.Y = this.target.Y - (this.zoomDist * this.look.Y);
                    this.position.Z = this.target.Z - (this.zoomDist * this.look.Z);
                    break;                
            }
        }

        public void RebuildYawPitch()
        {            
            if (look.X < 0)
            {
                Yaw = (float)(Math.PI - Math.Acos(look.Z));
            }
            else
            {
                Yaw = (float)(Math.PI + Math.Acos(look.Z));
            }
            pitch = (float)Math.Asin(look.Y);
        }

        /// <summary>
        /// Update camera
        /// </summary>
        public virtual void Update()
        {
            if (changed)
            {
                Process();
                view = Matrix.CreateLookAt(position, target, up);
                viewProjection = view * projection;
                frustum.Matrix = viewProjection;                

                viewInverseCreated = false;
                viewInverseTransposeCreated = false;
                RaiseCameraChanged();
                this.changed = false;
            }
        }
        #endregion        
    }    
}
